﻿namespace Fluid.SiteReader.Client
{
    using Fluid.SiteReader.Client.Extensions;
    using HtmlAgilityPack;
    using iTextSharp.text;
    using iTextSharp.text.pdf;
    using Pechkin;
    using Pechkin.Synchronized;
    using System;
    using System.Collections.Generic;
    using System.IO;
    using System.Linq;
    using System.Net;
    using System.Threading;
    using System.Threading.Tasks;

    public abstract class Graph : IGraph
    {
        //## stop/continue

        #region Static Members

        protected static readonly string WORKING_FOLDER = Properties.Settings.Default.WorkingFolder;

        private static readonly GlobalConfig PDF_GLOBAL_CONFIG = new GlobalConfig()
                                                                        .SetOutputDpi(Properties.Settings.Default.OutputDpi)
                                                                        .SetPaperSize(Properties.Settings.Default.PaperSize)
                                                                        .SetPaperOrientation(Properties.Settings.Default.IsPrintingLandscape);

        private static readonly ObjectConfig PDF_OBJECT_CONFIG = new ObjectConfig().SetCreateInternalLinks(false);

        private static readonly Dictionary<Graph, int> BOOKMARKS = new Dictionary<Graph, int>();

        private static readonly Dictionary<string, string> IMAGE_DICTIONARY = new Dictionary<string, string>();

        private static readonly StreamWriter LOG;

        private static readonly SemaphoreSlim THREAD_CONTROL = new SemaphoreSlim(Properties.Settings.Default.ThreadLimit);

        private static readonly object FILEGEN_LOCK = new object();

        #endregion

        #region Instance Members

        public Graph Parent { get; protected set; }
        public int Level { get; protected set; }
        public Uri Link { get; protected set; }
        public string LocalPath { get; protected set; }
        public string Title { get; protected set; }
        public bool EndOfLine { get; protected set; }
        public List<Graph> Children { get; protected set; }
        public HtmlNode Html { get; protected set; }

        #endregion

        #region Constructors

        static Graph()
        {
            if (Directory.Exists(WORKING_FOLDER))
                Directory.GetFiles(WORKING_FOLDER).ToList().ForEach(file => File.Delete(file));
            else
                Directory.CreateDirectory(WORKING_FOLDER);

            LOG = new StreamWriter(Path.Combine(Properties.Settings.Default.WorkingFolder, Properties.Settings.Default.LogFileName), false);
        }

        protected Graph()
        {
            this.LocalPath = GetRandomFile("pdf");
            this.Children = new List<Graph>();
        }

        protected Graph(Uri uri)
        {
            this.Level = 0;
            this.Link = uri;
            this.LocalPath = GetRandomFile("pdf");
            this.Parent = null;
            this.EndOfLine = false;
            this.Children = new List<Graph>();

            //this.Html = GetHtml();
            //this.Title = GetTitle();
        }

        #endregion

        #region Public Methods

        public Graph Fetch()
        {
            Fetch(1);
            return this;
        }

        public void WritePdf()
        {
            WritePdf(null);
        }

        public void WritePdf(string filename)
        {
            if (Properties.Settings.Default.IsUsingMultipleThreads)
                CalculateBookmarkPosition(this, 1);

            if (string.IsNullOrWhiteSpace(filename))
                filename = this.Title + ".pdf";

            var document = new Document();
            var writer = new PdfCopy(document, new FileStream(filename, FileMode.Create));
            document.Open();

            if (document != null && writer != null)
            {
                MergePdf(writer, writer.DirectContent.RootOutline, this);
                writer.SetOpenAction(PdfAction.GotoLocalPage(1, new PdfDestination(PdfDestination.FITBH), writer));
                writer.ViewerPreferences = PdfWriter.PageModeUseOutlines + PdfWriter.PageLayoutTwoPageLeft;
                document.Close();
            }
        }

        #endregion

        #region Abstract Methods

        protected virtual void CleanupHtml()
        { }

        protected virtual string GetTitle()
        {
            return this.Html.SelectSingleNode("/html/head/title").InnerText;
        }

        protected abstract IEnumerable<Graph> FindChildren();

        #endregion

        #region Private Methods

        private HtmlNode GetHtml()
        {
            HtmlNode html;
            HtmlWeb web = new HtmlWeb();
            html = web.Load(this.Link.AbsoluteUri).DocumentNode;
            web = null;

            return html;
        }

        private int Fetch(int currentBookmarkPosition)
        {
            if (Properties.Settings.Default.IsUsingMultipleThreads)
                THREAD_CONTROL.Wait(Properties.Settings.Default.ThreadTimeoutSeconds * 1000);

            List<Task> getChildrenTasks = new List<Task>();

            //get html and set title
            if (this.Html == null)
                this.Html = GetHtml();
            if (string.IsNullOrWhiteSpace(this.Title))
                this.Title = GetTitle();

            //write log
            for (int index = 0; index < this.Level; index++)
                LOG.Write('-');
            LOG.WriteLine(this.Title ?? "");
            LOG.Flush();

            //find children
            if (!this.EndOfLine)
                FindChildren().ForEachLazy(g => this.Children.Add(g));

            //write pdf
            CleanupHtml();
            FixGifPrinting();

            if (Properties.Settings.Default.IsOutputingHtml)
                using (var writer = new StreamWriter(Path.ChangeExtension(this.LocalPath, "html")))
                    writer.Write(this.Html.InnerHtml);

            //write pdf file
            using (var writer = new BinaryWriter(new StreamWriter(this.LocalPath).BaseStream))
                writer.Write(new SynchronizedPechkin(PDF_GLOBAL_CONFIG).Convert(PDF_OBJECT_CONFIG, this.Html.InnerHtml));
            this.Html = null;

            if (Properties.Settings.Default.IsUsingMultipleThreads)
                THREAD_CONTROL.Release();

            if (!Properties.Settings.Default.IsUsingMultipleThreads)
            {
                //update bookmark
                BOOKMARKS.Add(this, currentBookmarkPosition);
                var reader = new PdfReader(this.LocalPath);
                currentBookmarkPosition += reader.NumberOfPages;
                reader.Close();
                reader = null;
            }

            //parse children
            if (!this.EndOfLine)
                this.Children.ForEach(g =>
                {
                    if (Properties.Settings.Default.IsUsingMultipleThreads)
                        getChildrenTasks.Add(Task.Factory.StartNew(() => currentBookmarkPosition = g.Fetch(currentBookmarkPosition)));
                    else
                        currentBookmarkPosition = g.Fetch(currentBookmarkPosition);
                });

            //wait for all children
            Task.WaitAll(getChildrenTasks.ToArray());
            GC.Collect();

            return currentBookmarkPosition;
        }

        private void FixGifPrinting()
        {
            //fix issue (inherited bug) with gif not printing
            this.Html.Descendants("img").Where(d => d.Attributes["src"] != null && d.Attributes["src"].Value.ToLower().EndsWith(".gif"))
                .ForEachLazy(imageLink =>
                {
                    string gifUrlFile = imageLink.Attributes["src"].Value;
                    string gifLocalFile = GetRandomFile("gif");
                    string pngLocalFile = Path.ChangeExtension(gifLocalFile, "png");

                    lock (IMAGE_DICTIONARY)
                        if (!IMAGE_DICTIONARY.Keys.Contains(gifUrlFile))
                            using (WebClient client = new WebClient())
                            {
                                client.DownloadFile(gifUrlFile, gifLocalFile);
                                using (System.Drawing.Image localImage = System.Drawing.Image.FromFile(gifLocalFile))
                                    localImage.Save(pngLocalFile, System.Drawing.Imaging.ImageFormat.Png);
                                IMAGE_DICTIONARY.Add(gifUrlFile, new Uri(Path.GetFullPath(pngLocalFile)).AbsoluteUri);
                            }

                    imageLink.Attributes["src"].Value = IMAGE_DICTIONARY[gifUrlFile];
                });
        }

        #endregion

        #region Helper Methods

        private static string GetRandomFile(string extension)
        {
            lock (FILEGEN_LOCK)
            {
                string filename = null;
                while (filename == null || File.Exists(filename))
                    filename = Path.Combine(WORKING_FOLDER, Path.ChangeExtension(Path.GetRandomFileName(), extension));
                return filename;
            }
        }

        private static int CalculateBookmarkPosition(Graph graph, int currentBookmarkPosition)
        {
            BOOKMARKS.Add(graph, currentBookmarkPosition);

            var reader = new PdfReader(graph.LocalPath);
            currentBookmarkPosition += reader.NumberOfPages;
            reader.Close();

            foreach (var child in graph.Children)
                currentBookmarkPosition = CalculateBookmarkPosition(child, currentBookmarkPosition);

            return currentBookmarkPosition;
        }

        private static void MergePdf(PdfCopy writer, PdfOutline outline, Graph graph)
        {
            var reader = new PdfReader(graph.LocalPath);
            for (int index = 1; index <= reader.NumberOfPages; index++)
            {
                var pageDictionary = reader.GetPageN(index);
                var annotations = pageDictionary.GetAsArray(PdfName.ANNOTS);
                if (annotations != null)
                    foreach (PdfObject item in annotations.ArrayList)
                    {
                        var link = (PdfReader.GetPdfObject(item) as PdfDictionary).Get(PdfName.A) as PdfDictionary;
                        if (link != null && link.Get(PdfName.URI) as PdfString != null)
                        {
                            string uri = link.GetAsString(PdfName.URI).ToString();
                            var linkedGraph = BOOKMARKS.Keys
                                                        .Where(g => string.Equals(uri, g.Link.AbsoluteUri, StringComparison.InvariantCultureIgnoreCase))
                                                        .OrderByDescending(g => g.Children.Count)
                                                        .FirstOrDefault();
                            if (linkedGraph != null)
                            {
                                link.Clear();
                                var destination = PdfAction.GotoLocalPage(BOOKMARKS[linkedGraph], new PdfDestination(PdfDestination.FITBV), writer);
                                link.Put(PdfName.S, destination.Get(PdfName.S));
                                link.Put(PdfName.D, destination.Get(PdfName.D));
                            }
                        }
                    }

                writer.AddPage(writer.GetImportedPage(reader, index));
                if (index == 1)
                    outline = new PdfOutline(outline, PdfAction.GotoLocalPage(BOOKMARKS[graph], new PdfDestination(PdfDestination.FITBV), writer), graph.Title);
            }
            reader.Close();
            reader = null;

            foreach (var child in graph.Children)
                MergePdf(writer, outline, child);

            GC.Collect();
        }

        #endregion

    }
}
